Skip to content

Data-bound chart components 8/8: full mark-kind coverage - #469

Open
FarhanAliRaza wants to merge 6 commits into
stack/7-demos-and-galleryfrom
stack/8-full-kind-coverage
Open

Data-bound chart components 8/8: full mark-kind coverage#469
FarhanAliRaza wants to merge 6 commits into
stack/7-demos-and-galleryfrom
stack/8-full-kind-coverage

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Stacked on #467 (PR 8 of the reflex component-API stack). Review only the two commits here; everything below them is under review in #461#467.

What this changes

The data-bound plan tier shipped without the aggregating marks — box, violin, hexbin, contour, heatmap, stairs, ecdf. The Phase 3 decision excluded them because their validators need at least one finite value, and a synthetic-row probe "would validate against made-up data". This lifts that exclusion.

The objection is answered by discipline instead of exclusion. Each aggregating kind's channels bind fixed placeholder columns from a recorded table (plan._SYNTHETIC_CHANNELS) — finite, positive, strictly increasing, one group for the grouped kinds, a square z grid with matching side coordinates, len+1 bin edges. Those values satisfy every value-domain precondition a validator imposes, so a probe failure still indicts the structure, never the placeholders. Extending a kind means recording its minimal contract in that table rather than guessing silently (§28 spirit).

Riding along:

  • Flat factories for the newly probed kinds plus triangle_mesh_chart (always zero-row-safe, just never wired). All 19 standalone mark kinds now have one.
  • validate_columns drops the shared-length requirement. A data var may carry mixed-length and 2-D columns — a stairs mark's len+1 edges or a heatmap grid beside ordinary row columns. Coupled-shape contracts stay with the mark validators at bind, where the errors name the mark and channels involved.
  • Named callables content-address. A module-level callable in mark props (hexbin's reduce_C_function, np.mean by default) serializes as its import path. Lambdas and closures have no stable address and are refused toward a module-level function or @reflex_xy.figure. Plan registration becomes last-write-wins, so a hot reload replaces stale node objects behind an unchanged digest.
  • Demo: a /kinds page rendering every kind (data-bound flat factories for the 19 marks; composite kinds — pie, radar, sankey, polar, wind rose, facet — on the static tier, since those still take data directly), and §10 on the main page showing data=rx.cond(...) swapping two data vars under one fixed plan.

Spec

Recorded in three places rather than edited in place, so the reversal is legible: the implementation doc's post-landing revision section, the options doc's §8 decision record, and reflex-integration.md §3.6 "Kind coverage (recorded decision, revised 2026-08)".

Verification

uv run pytest tests/reflex_adapter tests/test_validation_timing.py tests/test_example_apps.py — 261 passed, 1 skipped (250 on #467). New pins: the shape table and per-kind plan builds (test_plan.py), the full 19-kind flat table (test_factories.py), SHAPED_ROW_CHARTS — the xy-level half of the contract (test_validation_timing.py), and the /kinds page composing with mixed-length + 2-D columns (test_example_apps.py).

pre-commit run --all-files, ruff check, ruff format --check, ty check all clean.

Not run: the browser E2E (scripts/reflex_ws_smoke.py) against the /kinds page. Worth doing before this merges — the probe proves the plans compile, not that all 19 kinds render.

Review in cubic

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 26911435-d196-4c69-8f2e-93a5094ffd09

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown

Greptile Summary

The PR expands data-bound plans to every standalone mark kind through empty structural probes and adds callable-aware plan addressing. It also adds flat Reflex factories, mixed-shape data support, and an all-kinds demo with browser-smoke coverage.

  • Adds structural-probe behavior for aggregating marks without fabricating validation data.
  • Adds data-bound factories and exports for box, violin, ECDF, hexbin, contour, heatmap, stairs, and triangle mesh.
  • Allows mixed-length and multidimensional columns while leaving coupled-shape checks to mark binding.
  • Adds callable serialization and last-write-wins plan registration, though pure-Python fingerprints still omit referenced global state.

Confidence Score: 4/5

The PR should not merge until callable digests account for behavior-affecting global state or reject reducers whose behavior is not fully addressable.

A supported module-level reducer can read mutable or worker-specific global state that is absent from its fingerprint, allowing one plan digest to execute different aggregation behavior across reloads or workers.

Files Needing Attention: python/reflex_xy/plan.py

Important Files Changed

Filename Overview
python/reflex_xy/plan.py Extends plans to structural probes and named callables, but the Python callable fingerprint does not capture behavior-affecting global values.
python/xy/marks.py Adds empty-data structural-probe exits while retaining real-data validation and aggregation paths.
python/reflex_xy/data_vars.py Removes the global equal-length restriction so individual mark validators can enforce their own coupled shapes.
python/reflex_xy/factories.py Adds flat data-bound factories for the remaining standalone mark kinds.
scripts/reflex_ws_smoke.py Adds /kinds navigation and per-cell canvas paint checks to the existing browser smoke harness.
examples/reflex/xy_reflex_demo/xy_reflex_demo.py Adds conditional data-source switching and a gallery route covering data-bound and static chart kinds.

Reviews (2): Last reviewed commit: "fix(reflex): zero-row structural plan pr..." | Re-trigger Greptile

Comment thread python/reflex_xy/plan.py Outdated
Comment thread python/reflex_xy/plan.py Outdated
Comment thread examples/reflex/xy_reflex_demo/xy_reflex_demo.py
@codspeed-hq

codspeed-hq Bot commented Aug 5, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 109 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing stack/8-full-kind-coverage (ce04ec4) with stack/7-demos-and-gallery (7c555e4)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 13 files

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread python/reflex_xy/plan.py Outdated
Comment thread python/reflex_xy/plan.py Outdated
Comment thread python/reflex_xy/plan.py Outdated
Comment thread python/reflex_xy/data_vars.py
Comment thread examples/reflex/xy_reflex_demo/xy_reflex_demo.py Outdated
@FarhanAliRaza
FarhanAliRaza force-pushed the stack/8-full-kind-coverage branch from d775fd1 to 362394c Compare August 6, 2026 13:40
@FarhanAliRaza FarhanAliRaza changed the title feat(reflex): full mark-kind coverage for the data-bound tier Data-bound chart components 8/8: full mark-kind coverage Aug 6, 2026
@FarhanAliRaza

Copy link
Copy Markdown
Contributor Author

Review addressed in b0437af + 362394c — both hard blocks are redesigned, not patched:

Synthetic validation is gone entirely. The fix is the core structural-validation path the review asked for: xy.structural_probe() (new in python/xy, spec'd in spec/api/chart-kind-contract.md "Structural probe"). Under it, a mark whose channels are all empty validates its configuration — enums, bounds, colormaps, range/level shapes — and skips aggregation; non-empty data behaves identically in and out of the mode, and outside it the aggregating validators keep their at-least-one-value contracts. build_plan compiles every kind zero-row; _SYNTHETIC_CHANNELS and the shape table are deleted. Your repros are pinned as tests: the stairs+scatter shared-edges composition builds a plan and binds real mixed-length data (test_plan.py::test_shared_columns_between_aggregating_and_zero_row_marks_probe), and hexbin range=((0,.5),(0,.5))/mincnt/gridsize=2048 probe without failing or allocating (::test_hexbin_value_dependent_configs_probe_without_aggregating). Config errors still fail reflex run for every aggregating kind (tests/test_validation_timing.py, three-way pins: compiles empty under probe / still refuses empty normally / still raises config errors under probe).

Callables are content-addressed, not name-addressed. Bound methods are refused outright (your two-reducers repro is pinned: test_bound_methods_are_refused_as_plan_callables). Pure-Python functions serialize as import path plus a code fingerprint (bytecode, names, nested code objects, defaults; frozenset consts sorted for cross-process determinism) — editing a reducer's body changes the digest, so a rolling deployment resyncs instead of executing two behaviors behind one address (test_callable_digest_follows_the_body_not_only_the_name builds two same-qualname bodies and asserts distinct digests). C-level callables (ufuncs/builtins) must resolve back to the same object by qualified name and carry their distribution's version. Closures and partials are refused.

Also: the @data docstring drops the stale equal-length claim, /kinds now carries browser render coverage (smoke step 7, all 26 cells pixel-probed; the full smoke passes against the running app), and the plan-tier benchmark from 5/8 covers the page-time/startup/republish costs. Specs revised across reflex-integration.md §3.6, chart-kind-contract.md, and the implementation/options decision records — the completion record now describes exactly what is implemented.

Comment thread python/reflex_xy/plan.py
@masenf
masenf force-pushed the stack/8-full-kind-coverage branch from 362394c to 53dab90 Compare August 6, 2026 22:08

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile has paused reviews on this repository — it used its 100 free open-source review credits for this billing period. Reviews resume automatically on September 3. To continue before then, an organization admin can keep reviews running past the free credits — those bill as normal usage.

FarhanAliRaza and others added 6 commits August 7, 2026 01:23
The data-bound plan tier refused the aggregating marks — box, violin,
hexbin, contour, heatmap, stairs, ecdf — because their validators need at
least one finite value and the Phase 3 decision judged a synthetic-row
probe to be validating against made-up data.

Answer that objection with discipline instead of exclusion. Each
aggregating kind's channels bind fixed placeholder columns from a recorded
table (plan._SYNTHETIC_CHANNELS): finite, positive, strictly increasing,
one group for the grouped kinds, a square z grid with matching side
coordinates, len+1 bin edges. The values satisfy every value-domain
precondition a validator imposes, so a probe failure still indicts the
structure and never the placeholders. Extending a kind means recording its
minimal contract in that table — never a silent guess (§28 spirit).

Landing with it:

- Flat factories for the newly probed kinds plus triangle_mesh_chart
  (always zero-row-safe, just never wired). All 19 standalone mark kinds
  now have one.
- validate_columns drops the shared-length requirement: a data var may
  carry mixed-length and 2-D columns (stairs edges, heatmap grids).
  Coupled-shape contracts stay with the mark validators at bind, where the
  errors name the mark and channels involved.
- Module-level named callables in mark props (hexbin's reduce_C_function,
  np.mean by default) content-address as their import path; lambdas and
  closures have no stable one and are refused toward a module-level
  function or @reflex_xy.figure. Plan registration is last-write-wins so a
  hot reload replaces stale node objects behind an unchanged digest.

Specs updated: the revision is recorded in the implementation doc's
post-landing section, the options doc's decision record, and
reflex-integration.md §3.6 "Kind coverage".
Two additions to the demo app, both covered by test_example_apps.py:

- A /kinds page rendering all 19 standalone mark kinds as data-bound flat
  factories fed by a single @reflex_xy.data var — mixed column lengths and
  a 2-D grid in one var, which the relaxed column validation now permits —
  next to the composite kinds (pie, radar, sankey, polar, polar bars, wind
  rose, facet) on the static tier, since those still take data directly.
- §10 on the main page: data=rx.cond(...) picking between two data vars.
  The cond sits at the data-var level, so one fixed plan swaps between two
  column sets from state and both branches stay compile-checked against
  the shared schema.
…er runs empty

xy.structural_probe() (spec/api/chart-kind-contract.md 'Structural
probe'): while active, a mark whose data channels are all empty
validates its configuration — enums, bounds, colormaps, range/level
shapes — and contributes no traces, instead of refusing zero rows or
aggregating. This is the core seam compile gates need to validate chart
structure with no data and no invented data; non-empty channels behave
identically in and out of the mode.

Each aggregating mark (stairs, ecdf, histogram, box, violin, hexbin,
contour, heatmap) orders config validation before its data work and
gates its zero-row refusal on the mode; _split_by_positions handles the
empty grouping case. Pinned three ways in test_validation_timing.py:
every kind compiles empty under the probe, still refuses empty
normally, and still raises config errors under the probe.
Review hard-blocked the synthetic-column probe as structurally unsound:
a column shared between an aggregating channel and a zero-row channel
falsely failed on invented lengths (stairs edges len 9 vs scatter's 0),
valid hexbin range=/mincnt= configurations falsely failed on invented
values, and large gridsize ran real aggregation at page evaluation.

- build_plan now compiles every kind zero-row under the core's new
  structural_probe() mode; _SYNTHETIC_CHANNELS and the shape table are
  gone. Config errors still fail reflex run; data-dependent outcomes
  and real-data shape couplings move (back) to bind. Repro pins:
  shared-column composition and hexbin range/mincnt/gridsize configs.
- Plan callables are content-addressed, not name-addressed: import path
  + code fingerprint (bytecode/names/nested code/defaults) for
  pure-Python functions — editing a reducer body changes the digest, so
  rolling deployments resync instead of diverging; import path +
  distribution version for C-level callables that resolve back to
  themselves; bound methods, lambdas, closures, and partials refused.
- @reflex_xy.data docstring drops the stale equal-length claim.
- /kinds gains browser render coverage: reflex_ws_smoke.py step 7
  navigates there and pixel-probes all 26 kind cells (19 data-bound +
  7 static composites), each addressable as kind-<name>.
…ingerprint boundary

cond_summary floor-clamped bin counts to dodge division by zero, so an
empty bin would render a mean of exactly 0 — a false origin point at
that x-centre. Empty bins are now dropped (columns simply shorten);
with the demo's 1M-point seed the output is unchanged, the promise is
now unconditional.

reflex-integration.md §3.6 records the callable-fingerprint boundary
explicitly: the digest addresses code, never process state — mutable
module globals in a reducer are the same purity contract every
server-side builder tier carries, and are not content-addressable.
The public-surface pin added in stack/5 (`test_public_surface.py`) requires
every name in `reflex_xy.__all__` to carry a static declaration alongside
its `__getattr__` route, so consumers get real signatures instead of `Any`.

This layer's eight new flat factories (box, contour, ecdf, heatmap, hexbin,
stairs, triangle_mesh, violin) and the newly curated `triangle_mesh` node
re-export were added to `_EXPORTS`/`_XY_REEXPORTS` only, so they typed as
missing. Restate them in the `TYPE_CHECKING` block.
@masenf
masenf force-pushed the stack/8-full-kind-coverage branch from 53dab90 to ce04ec4 Compare August 7, 2026 01:26

masenf commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Rebased onto the current stack/7-demos-and-gallery (ce04ec4). The five commits replayed without conflicts and their content is unchanged — I diffed the pre- and post-rebase ranges and the only differences are hunk offsets.

The rebase did surface one real failure, so there is one new commit on top that was not in the reviewed history:

test_public_surface.py::test_every_public_export_is_statically_typed was added by af9f99c on stack/5-chart-factories, after this branch was originally cut. It requires every name in reflex_xy.__all__ to carry a static declaration next to its __getattr__ route, so consumers see real signatures instead of Any. This layer's eight new flat factories (box_chart, contour_chart, ecdf_chart, heatmap_chart, hexbin_chart, stairs_chart, triangle_mesh_chart, violin_chart) and the newly curated triangle_mesh node re-export went into _EXPORTS/_XY_REEXPORTS only, so they typed as missing. ce04ec4 restates them in the TYPE_CHECKING block — declarations only, no runtime change.

Worth noting because it means the old green CI here was green against a lineage that did not yet contain that pin, not because the surface was complete.

Local verification on the rebased branch: tests/reflex_adapter tests/test_validation_timing.py tests/test_example_apps.py tests/test_check_typing.py — 312 passed, 1 skipped (311 + 1 failure before the fix). pre-commit run --all-files, ruff check, ruff format --check clean; ty check shows the same 7 pre-existing pyplot diagnostics that main has.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants